import { Connection, PublicKey, SystemProgram, TransactionInstruction } from '@solana/web3.js';
import { deserialize, Schema, serialize } from 'borsh';

import { deleteNameRegistry, NAME_PROGRAM_ID } from './bindings';
import { createInstruction, deleteInstruction, transferInstruction, updateInstruction } from './instructions';
import { NameRegistryState } from './state';
import { getFilteredProgramAccounts, getHashedName, getNameAccountKey, Numberu32, Numberu64 } from './utils';

////////////////////////////////////////////////////
// Global Variables

export const TWITTER_VERIFICATION_AUTHORITY = new PublicKey('FvPH7PrVrLGKPfqaf3xJodFTjZriqrAXXLTVWEorTFBi');
// The address of the name registry that will be a parent to all twitter handle registries,
// it should be owned by the TWITTER_VERIFICATION_AUTHORITY and its name is irrelevant
export const TWITTER_ROOT_PARENT_REGISTRY_KEY = new PublicKey('4YcexoW3r78zz16J2aqmukBLRwGq6rAvWzJpkYAXqebv');

////////////////////////////////////////////////////
// Bindings

// Signed by the authority, the payer and the verified pubkey
export async function createVerifiedTwitterRegistry(
    connection: Connection,
    twitterHandle: string,
    verifiedPubkey: PublicKey,
    space: number, // The space that the user will have to write data into the verified registry
    payerKey: PublicKey,
): Promise<TransactionInstruction[]> {
    // Create user facing registry
    const hashedTwitterHandle = await getHashedName(twitterHandle);
    const twitterHandleRegistryKey = await getNameAccountKey(
        hashedTwitterHandle,
        undefined,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );

    let instructions = [
        createInstruction(
            NAME_PROGRAM_ID,
            SystemProgram.programId,
            twitterHandleRegistryKey,
            verifiedPubkey,
            payerKey,
            hashedTwitterHandle,
            new Numberu64(await connection.getMinimumBalanceForRentExemption(space)),
            new Numberu32(space),
            undefined,
            TWITTER_ROOT_PARENT_REGISTRY_KEY,
            TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as owner of the parent for all user-facing registries
        ),
    ];

    instructions = instructions.concat(
        await createReverseTwitterRegistry(
            connection,
            twitterHandle,
            twitterHandleRegistryKey,
            verifiedPubkey,
            payerKey,
        ),
    );

    return instructions;
}

// Overwrite the data that is written in the user facing registry
// Signed by the verified pubkey
export async function changeTwitterRegistryData(
    twitterHandle: string,
    verifiedPubkey: PublicKey,
    offset: number, // The offset at which to write the input data into the NameRegistryData
    input_data: Buffer,
): Promise<TransactionInstruction[]> {
    const hashedTwitterHandle = await getHashedName(twitterHandle);
    const twitterHandleRegistryKey = await getNameAccountKey(
        hashedTwitterHandle,
        undefined,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );

    const instructions = [
        updateInstruction(
            NAME_PROGRAM_ID,
            twitterHandleRegistryKey,
            new Numberu32(offset),
            input_data,
            verifiedPubkey,
            undefined,
        ),
    ];

    return instructions;
}

// Change the verified pubkey for a given twitter handle
// Signed by the Authority, the verified pubkey and the payer
export async function changeVerifiedPubkey(
    connection: Connection,
    twitterHandle: string,
    currentVerifiedPubkey: PublicKey,
    newVerifiedPubkey: PublicKey,
    payerKey: PublicKey,
): Promise<TransactionInstruction[]> {
    const hashedTwitterHandle = await getHashedName(twitterHandle);
    const twitterHandleRegistryKey = await getNameAccountKey(
        hashedTwitterHandle,
        undefined,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );

    // Transfer the user-facing registry ownership
    let instructions = [
        transferInstruction(
            NAME_PROGRAM_ID,
            twitterHandleRegistryKey,
            newVerifiedPubkey,
            currentVerifiedPubkey,
            undefined,
        ),
    ];

    // Delete the current reverse registry
    instructions.push(
        await deleteNameRegistry(
            connection,
            currentVerifiedPubkey.toString(),
            payerKey,
            TWITTER_VERIFICATION_AUTHORITY,
            TWITTER_ROOT_PARENT_REGISTRY_KEY,
        ),
    );

    // Create the new reverse registry
    instructions = instructions.concat(
        await createReverseTwitterRegistry(
            connection,
            twitterHandle,
            twitterHandleRegistryKey,
            newVerifiedPubkey,
            payerKey,
        ),
    );

    return instructions;
}

// Delete the verified registry for a given twitter handle
// Signed by the verified pubkey
export async function deleteTwitterRegistry(
    twitterHandle: string,
    verifiedPubkey: PublicKey,
): Promise<TransactionInstruction[]> {
    const hashedTwitterHandle = await getHashedName(twitterHandle);
    const twitterHandleRegistryKey = await getNameAccountKey(
        hashedTwitterHandle,
        undefined,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );

    const hashedVerifiedPubkey = await getHashedName(verifiedPubkey.toString());
    const reverseRegistryKey = await getNameAccountKey(
        hashedVerifiedPubkey,
        TWITTER_VERIFICATION_AUTHORITY,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );

    const instructions = [
        // Delete the user facing registry
        deleteInstruction(NAME_PROGRAM_ID, twitterHandleRegistryKey, verifiedPubkey, verifiedPubkey),
        // Delete the reverse registry
        deleteInstruction(NAME_PROGRAM_ID, reverseRegistryKey, verifiedPubkey, verifiedPubkey),
    ];

    return instructions;
}

//////////////////////////////////////////
// Getter Functions

// Returns the key of the user-facing registry
export async function getTwitterRegistryKey(twitter_handle: string): Promise<PublicKey> {
    const hashedTwitterHandle = await getHashedName(twitter_handle);
    return await getNameAccountKey(hashedTwitterHandle, undefined, TWITTER_ROOT_PARENT_REGISTRY_KEY);
}

export async function getTwitterRegistry(connection: Connection, twitter_handle: string): Promise<NameRegistryState> {
    const hashedTwitterHandle = await getHashedName(twitter_handle);
    const twitterHandleRegistryKey = await getNameAccountKey(
        hashedTwitterHandle,
        undefined,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );
    const registry = NameRegistryState.retrieve(connection, twitterHandleRegistryKey);
    return registry;
}

export async function getHandleAndRegistryKey(
    connection: Connection,
    verifiedPubkey: PublicKey,
): Promise<[string, PublicKey]> {
    const hashedVerifiedPubkey = await getHashedName(verifiedPubkey.toString());
    const reverseRegistryKey = await getNameAccountKey(
        hashedVerifiedPubkey,
        TWITTER_VERIFICATION_AUTHORITY,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );

    const reverseRegistryState = await ReverseTwitterRegistryState.retrieve(connection, reverseRegistryKey);
    return [reverseRegistryState.twitterHandle, new PublicKey(reverseRegistryState.twitterRegistryKey)];
}

// Uses the RPC node filtering feature, execution speed may vary
export async function getTwitterHandleandRegistryKeyViaFilters(
    connection: Connection,
    verifiedPubkey: PublicKey,
): Promise<[string, PublicKey]> {
    const filters = [
        {
            memcmp: {
                offset: 0,
                bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBase58(),
            },
        },
        {
            memcmp: {
                offset: 32,
                bytes: verifiedPubkey.toBase58(),
            },
        },
        {
            memcmp: {
                offset: 64,
                bytes: TWITTER_VERIFICATION_AUTHORITY.toBase58(),
            },
        },
    ];

    const filteredAccounts = await getFilteredProgramAccounts(connection, NAME_PROGRAM_ID, filters);

    for (const f of filteredAccounts) {
        if (f.accountInfo.data.length > NameRegistryState.HEADER_LEN + 32) {
            const data = f.accountInfo.data.slice(NameRegistryState.HEADER_LEN);
            const state = deserialize(ReverseTwitterRegistryState.schema, data) as ReverseTwitterRegistryState;
            return [state.twitterHandle, new PublicKey(state.twitterRegistryKey)];
        }
    }
    throw new Error('Registry not found.');
}

// Uses the RPC node filtering feature, execution speed may vary
// Does not give you the handle, but is an alternative to getHandlesAndKeysFromVerifiedPubkey + getTwitterRegistry to get the data
export async function getTwitterRegistryData(connection: Connection, verifiedPubkey: PublicKey): Promise<Buffer> {
    const filters = [
        {
            memcmp: {
                offset: 0,
                bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBase58(),
            },
        },
        {
            memcmp: {
                offset: 32,
                bytes: verifiedPubkey.toBase58(),
            },
        },
        {
            memcmp: {
                offset: 64,
                bytes: new PublicKey(Buffer.alloc(32, 0)).toBase58(),
            },
        },
    ];

    const filteredAccounts = await getFilteredProgramAccounts(connection, NAME_PROGRAM_ID, filters);

    if (filteredAccounts.length > 1) {
        throw new Error('Found more than one registry.');
    }

    return filteredAccounts[0].accountInfo.data.slice(NameRegistryState.HEADER_LEN);
}

//////////////////////////////////////////////
// Utils

export class ReverseTwitterRegistryState {
    twitterRegistryKey: Uint8Array;
    twitterHandle: string;

    static schema: Schema = {
        struct: {
            twitterRegistryKey: { array: { type: 'u8', len: 32 } },
            twitterHandle: 'string',
        },
    };
    constructor(obj: { twitterRegistryKey: Uint8Array; twitterHandle: string }) {
        this.twitterRegistryKey = obj.twitterRegistryKey;
        this.twitterHandle = obj.twitterHandle;
    }

    public static async retrieve(
        connection: Connection,
        reverseTwitterAccountKey: PublicKey,
    ): Promise<ReverseTwitterRegistryState> {
        const reverseTwitterAccount = await connection.getAccountInfo(reverseTwitterAccountKey, 'processed');
        if (!reverseTwitterAccount) {
            throw new Error('Invalid reverse Twitter account provided');
        }

        const res = deserialize(
            this.schema,
            reverseTwitterAccount.data.slice(NameRegistryState.HEADER_LEN),
        ) as ReverseTwitterRegistryState;

        return res;
    }
}

export async function createReverseTwitterRegistry(
    connection: Connection,
    twitterHandle: string,
    twitterRegistryKey: PublicKey,
    verifiedPubkey: PublicKey,
    payerKey: PublicKey,
): Promise<TransactionInstruction[]> {
    // Create the reverse lookup registry
    const hashedVerifiedPubkey = await getHashedName(verifiedPubkey.toString());
    const reverseRegistryKey = await getNameAccountKey(
        hashedVerifiedPubkey,
        TWITTER_VERIFICATION_AUTHORITY,
        TWITTER_ROOT_PARENT_REGISTRY_KEY,
    );
    const reverseTwitterRegistryStateBuff = serialize(
        ReverseTwitterRegistryState.schema,
        new ReverseTwitterRegistryState({
            twitterRegistryKey: twitterRegistryKey.toBytes(),
            twitterHandle,
        }),
    );
    return [
        createInstruction(
            NAME_PROGRAM_ID,
            SystemProgram.programId,
            reverseRegistryKey,
            verifiedPubkey,
            payerKey,
            hashedVerifiedPubkey,
            new Numberu64(await connection.getMinimumBalanceForRentExemption(reverseTwitterRegistryStateBuff.length)),
            new Numberu32(reverseTwitterRegistryStateBuff.length),
            TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as class for all reverse-lookup registries
            TWITTER_ROOT_PARENT_REGISTRY_KEY, // Reverse registries are also children of the root
            TWITTER_VERIFICATION_AUTHORITY,
        ),
        updateInstruction(
            NAME_PROGRAM_ID,
            reverseRegistryKey,
            new Numberu32(0),
            Buffer.from(reverseTwitterRegistryStateBuff),
            TWITTER_VERIFICATION_AUTHORITY,
            undefined,
        ),
    ];
}
